Skip to content

Redesign Email Server settings + managed domain flow#1373

Merged
BilalG1 merged 2 commits intodevfrom
email-server-ui-variations
Apr 24, 2026
Merged

Redesign Email Server settings + managed domain flow#1373
BilalG1 merged 2 commits intodevfrom
email-server-ui-variations

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Apr 22, 2026

Summary

Rewrites the Email Server section of the project email settings page and the managed-domain setup flow. Replaces the dropdown + conditional-fields layout with a visual four-card picker, a clearer unsaved-state model, a stepper dialog for managed-domain onboarding, and a consistent tracked-domains list. Also fixes two data-correctness bugs in the managed-domain backend.

Walkthrough (2×, dead-frames trimmed)

walkthrough

Before

The saved state was a minimal dropdown, but choosing Custom SMTP / Resend revealed a long conditional form with a hidden gear toggle for server config, no clear "what is saved" signal, and a separate dialog pattern for managed domains.

Saved (Managed) Custom SMTP selected
before-managed before-smtp

After — Provider cards

Four visual cards (Stack Shared, Managed Domain, Resend, Custom SMTP) with updated copy. The saved provider shows a green Current pill; the card the user is previewing shows an amber dashed Draft pill. An amber unsaved-changes banner appears between the picker and the form when state diverges from saved, so it is unambiguous that a click is not yet committed.

Saved state Previewing a different provider
after-saved after-draft

Copy changes:

  • Stack Shared — "Only default emails — no custom templates, themes, or sender identity." (was: "Shared (noreply@stackframe.co)")
  • Managed Domain — "Bring your own domain. You add DNS records; we handle signing & delivery." (was: "Managed (via managed domain setup)")
  • Resend uses the official Resend brand mark (light/dark variants in apps/dashboard/public/assets/)

After — Managed domain list + stepper dialog

Selecting Managed Domain immediately shows the tracked-domain list with an Add domain button. Each row reflects real status (Active / Verified / Waiting for DNS / Verifying / Failed). Exactly one domain can be Active — the one matching the saved email config; every other verified/applied domain shows a Use this domain button so switching is always possible.

Adding a domain opens a 3-stage dialog with a horizontal stepper (Verify is right-aligned for the final step). Stage 2 replaces the old bare NS-list with a proper Type / Name / Content DNS records table with per-row copy buttons.

Tracked domains list DNS records table
after-list after-dns-table

Bug fixes

  • Backend: applying a managed domain did not demote previously-applied ones. Multiple rows could end up with status APPLIED even though only one could be in the saved config. New helper demoteOtherAppliedManagedEmailDomains({ tenancyId, keepId }) runs inside applyManagedEmailProvider to demote all other applied rows in the tenancy back to VERIFIED before marking the new one.
  • Frontend: "Use this domain" only appeared for status === verified. A domain that had been applied then replaced could never be re-applied from the UI. Button now appears for any verified or applied row that is not currently in use; the Active label is derived from config match instead of DB status.
  • Dev mock onboarding now mirrors production timing. shouldUseMockManagedEmailOnboarding() used to insert domains as verified synchronously. Now the domain is created as pending_verification, and a fire-and-forget runAsynchronously(() => wait(1000)) updates it to verified — mirroring the real Resend webhook flow so the UI states (pending → verifying → verified) are exercised in local dev.

Test plan

  • Cards: clicking each card shows Draft pill + amber banner; Discard restores; Save commits and flips Current to the new card
  • Managed: Add domain → stage 1 input → stage 2 DNS table + copy → Check verification flips to stage 3 → Use this domain sets it Active and demotes the previously-active domain in the list
  • Managed: clicking Use this domain on a non-active verified row makes it Active and the previously-active row back to Verified
  • Shared / Resend / SMTP: existing save + test-email flows still work (logic preserved verbatim)
  • pnpm typecheck (dashboard + backend) and pnpm lint pass

Summary by CodeRabbit

  • New Features

    • Redesigned email domain setup flow with multi-step verification dialog
    • Added copy-to-clipboard for DNS records
    • Enhanced provider selection interface with improved visual presentation
    • Onboarding now shows initial "pending verification" state and completes verification asynchronously
  • Bug Fixes

    • Ensures only one managed domain becomes active when applying a domain
    • Improved error handling for email configuration saves
  • Tests

    • Updated end-to-end tests to reflect async verification timing

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Apr 23, 2026 0:18am
stack-backend Ready Ready Preview, Comment Apr 23, 2026 0:18am
stack-dashboard Ready Ready Preview, Comment Apr 23, 2026 0:18am
stack-demo Ready Ready Preview, Comment Apr 23, 2026 0:18am
stack-docs Ready Ready Preview, Comment Apr 23, 2026 0:18am
stack-preview-backend Ready Ready Preview, Comment Apr 23, 2026 0:18am
stack-preview-dashboard Ready Ready Preview, Comment Apr 23, 2026 0:18am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa6c5fd9-2a55-4a3a-a062-9a43aaa74a38

📥 Commits

Reviewing files that changed from the base of the PR and between 459589c and 0309df8.

📒 Files selected for processing (1)
  • apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts

📝 Walkthrough

Walkthrough

Adds a backend helper to demote other applied managed email domains, updates onboarding to create domains as pending then asynchronously verify them, and refactors the dashboard domain settings into a provider-card UX with a multi-step ManagedDomainSetupDialog.

Changes

Cohort / File(s) Summary
Backend Demotion
apps/backend/src/lib/managed-email-domains.tsx
New exported async demoteOtherAppliedManagedEmailDomains({ tenancyId, keepId }) that sets other APPLIED domains to VERIFIED, clears appliedAt, and updates updatedAt, excluding keepId.
Onboarding Flow
apps/backend/src/lib/managed-email-onboarding.tsx
Mock onboarding now creates domains with status: "pending_verification" and schedules an async webhook call to transition to verified. Applying a domain now calls the new demotion helper before marking the chosen domain as applied.
Dashboard UI (Domain Settings)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Large rewrite: replaces legacy server config with provider-card selection, adds ManagedDomainSetupDialog (multi-step validation, DNS records with copy, explicit verify/use flows), local state for domains, auto-refresh when switching to managed, and updated status labels/colors.
E2E Test Update
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
Tests updated to expect initial pending_verification and to wait (~1.5s) for the async verification webhook before asserting verified state.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / Dashboard
    participant Dashboard as Dashboard UI
    participant Backend as Backend API
    participant DB as Database

    User->>Dashboard: Choose provider & click "Use this domain"
    Dashboard->>Backend: POST /apply-managed-domain (domainId)
    Backend->>DB: SELECT APPLIED domains WHERE tenancyId = X AND id != keepId
    DB-->>Backend: return list
    Backend->>DB: UPDATE set status=VERIFIED, appliedAt=NULL, updatedAt=now() FOR each
    DB-->>Backend: updated
    Backend->>DB: UPDATE chosen domain set status=APPLIED, appliedAt=now()
    DB-->>Backend: updated
    Backend-->>Dashboard: 200 OK
    Dashboard->>User: show active domain
Loading
sequenceDiagram
    participant Onboarding as Onboarding Flow
    participant Backend as Backend API
    participant DB as Database
    participant Webhook as Webhook Handler

    Onboarding->>Backend: POST /create-managed-domain (status=pending_verification)
    Backend->>DB: INSERT domain(status=pending_verification)
    DB-->>Backend: created domain
    Backend->>Backend: schedule async (1s) invoke
    Backend->>Webhook: updateManagedEmailDomainWebhookStatus(domainId, providerStatusRaw=verified)
    Webhook->>DB: UPDATE domain set status=verified, providerStatusRaw=verified
    DB-->>Webhook: updated
    Webhook-->>Backend: confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • N2D4

Poem

🐰 I hopped through code and nudged domains down,
One stays crowned, the others wear a new gown.
Cards and dialogs bloom where settings once slept,
A tiny async webhook quietly leapt.
Hooray for domains—now orderly, not brown! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: a comprehensive redesign of the Email Server settings UI and the managed domain workflow with new card-based provider selection and stepper dialog.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, visual before/after comparisons, detailed walkthrough of UI changes, backend and frontend bug fixes, development mock improvements, and a test plan with checkbox items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch email-server-ui-variations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR redesigns the Email Server settings page with a four-card provider picker, a stepper dialog for managed-domain onboarding, and a DNS records table with copy buttons. It also fixes two backend bugs: multiple domains could remain APPLIED simultaneously, and the "Use this domain" button was gated only on status === "verified" rather than also allowing applied rows to be re-applied.

  • P1 — Race condition in applyManagedEmailProvider: demoteOtherAppliedManagedEmailDomains and markManagedEmailDomainApplied run as two separate queries with no enclosing transaction, meaning concurrent apply calls for the same tenancy can still produce multiple APPLIED rows — the exact bug the fix is targeting.

Confidence Score: 4/5

Safe to merge after addressing the missing transaction wrapping the demote + mark pair in applyManagedEmailProvider.

One P1 finding — the demote and mark operations are not atomic, leaving a race window that reproduces the bug being fixed under concurrent load. The two P2 findings (stale emailConfig after domain apply, dropped SMTP field-level validation) are non-blocking UX issues.

apps/backend/src/lib/managed-email-onboarding.tsx — the applyManagedEmailProvider function needs its demote + mark pair wrapped in a Prisma $transaction.

Important Files Changed

Filename Overview
apps/backend/src/lib/managed-email-domains.tsx Adds demoteOtherAppliedManagedEmailDomains — a targeted UPDATE that resets competing APPLIED rows back to VERIFIED; logic is correct but not transactionally coupled with the subsequent markManagedEmailDomainApplied call in the onboarding file.
apps/backend/src/lib/managed-email-onboarding.tsx Wires in the new demote helper before marking a domain applied, and changes mock onboarding to start as pending_verification then asynchronously flip to verified; the demote+mark pair is not transactional (race window) and the fire-and-forget update has no error handling.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx Major UI redesign replacing the dropdown + hidden-fields pattern with a four-card provider picker, a stepper dialog for managed-domain onboarding, and a domain list with per-row status badges. Core logic is sound; minor issues around stale emailConfig after domain apply and dropped inline SMTP field validation.
apps/dashboard/public/assets/resend-icon-black.svg Adds the official Resend brand SVG (black variant) for the light-mode provider card.
apps/dashboard/public/assets/resend-icon-white.svg Adds the official Resend brand SVG (white variant) for the dark-mode provider card.

Sequence Diagram

sequenceDiagram
    participant U as User (Dashboard)
    participant DS as DomainSettings
    participant SDK as stackAdminApp SDK
    participant BE as Backend API

    U->>DS: Click "Add domain"
    DS->>DS: Open ManagedDomainSetupDialog (stage 1)
    U->>DS: Enter subdomain + sender, click Continue
    DS->>SDK: setupManagedEmailProvider(subdomain, senderLocalPart)
    SDK->>BE: POST /managed-email/setup
    BE-->>SDK: { domainId, nameServerRecords, status: pending_verification }
    SDK-->>DS: SetupState
    DS->>DS: Advance to stage 2 (DNS records table)
    U->>DS: Click "Check verification"
    DS->>SDK: checkManagedEmailStatus(domainId)
    SDK->>BE: GET /managed-email/status
    BE-->>SDK: { status: verified }
    SDK-->>DS: status
    DS->>DS: Advance to stage 3 (Domain verified)
    U->>DS: Click "Use this domain"
    DS->>SDK: applyManagedEmailProvider(domainId)
    SDK->>BE: POST /managed-email/apply
    BE->>BE: demoteOtherAppliedManagedEmailDomains (query 1)
    BE->>BE: markManagedEmailDomainApplied (query 2)
    Note over BE: No transaction — race window between queries
    BE-->>SDK: { status: applied }
    SDK-->>DS: success
    DS->>DS: toast + close dialog
    DS->>SDK: listManagedEmailDomains (refreshDomains)
    SDK-->>DS: updated domain list
    DS->>DS: Re-render list (emailConfig not re-fetched)
Loading

Comments Outside Diff (3)

  1. apps/backend/src/lib/managed-email-onboarding.tsx, line 642-74 (link)

    P1 Missing transaction wrapping demote + mark operations

    demoteOtherAppliedManagedEmailDomains and markManagedEmailDomainApplied execute as two separate raw queries with no enclosing transaction. If two concurrent applyManagedEmailProvider calls arrive for the same tenancy (e.g., two browser tabs clicking "Use this domain" simultaneously), the sequence can interleave:

    1. Call A demotes Y → VERIFIED
    2. Call B demotes X → VERIFIED
    3. Call A marks X → APPLIED
    4. Call B marks Y → APPLIED

    …leaving both X and Y APPLIED — exactly the bug this PR was meant to fix. Wrapping both operations in a $transaction (or using a single atomic UPDATE … RETURNING pattern) would close this window.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/backend/src/lib/managed-email-onboarding.tsx
    Line: 642-74
    
    Comment:
    **Missing transaction wrapping demote + mark operations**
    
    `demoteOtherAppliedManagedEmailDomains` and `markManagedEmailDomainApplied` execute as two separate raw queries with no enclosing transaction. If two concurrent `applyManagedEmailProvider` calls arrive for the same tenancy (e.g., two browser tabs clicking "Use this domain" simultaneously), the sequence can interleave:
    
    1. Call A demotes Y → VERIFIED
    2. Call B demotes X → VERIFIED
    3. Call A marks X → APPLIED
    4. Call B marks Y → APPLIED
    
    …leaving both X and Y `APPLIED` — exactly the bug this PR was meant to fix. Wrapping both operations in a `$transaction` (or using a single atomic `UPDATE … RETURNING` pattern) would close this window.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx, line 1326-1348 (link)

    P2 SMTP config fields lost inline validation feedback

    The old render loop included className={cn(isDirty && isEmpty && "border-destructive")} and an inline {field.label} is required error for each empty SMTP/Resend field. The new code renders the same fields without any per-field error indicator. The save button is still correctly disabled via canSave, but users see no visual cue explaining why — especially problematic for the four SMTP fields (host, port, username, password) where any one being blank silently blocks saving.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
    Line: 1326-1348
    
    Comment:
    **SMTP config fields lost inline validation feedback**
    
    The old render loop included `className={cn(isDirty && isEmpty && "border-destructive")}` and an inline `{field.label} is required` error for each empty SMTP/Resend field. The new code renders the same fields without any per-field error indicator. The save button is still correctly disabled via `canSave`, but users see no visual cue explaining why — especially problematic for the four SMTP fields (host, port, username, password) where any one being blank silently blocks saving.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. apps/backend/src/lib/managed-email-onboarding.tsx, line 55-63 (link)

    P2 Fire-and-forget domain status update has no error handling in mock path

    runAsynchronously is used without a .catch or onError handler. If updateManagedEmailDomainWebhookStatus throws (e.g., the row was deleted before the 1-second delay completes), the error is silently swallowed, leaving the mock domain permanently in pending_verification. This is dev-only code, but it can cause confusing failures during local testing.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/backend/src/lib/managed-email-onboarding.tsx
    Line: 55-63
    
    Comment:
    **Fire-and-forget domain status update has no error handling in mock path**
    
    `runAsynchronously` is used without a `.catch` or `onError` handler. If `updateManagedEmailDomainWebhookStatus` throws (e.g., the row was deleted before the 1-second delay completes), the error is silently swallowed, leaving the mock domain permanently in `pending_verification`. This is dev-only code, but it can cause confusing failures during local testing.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/backend/src/lib/managed-email-onboarding.tsx
Line: 642-74

Comment:
**Missing transaction wrapping demote + mark operations**

`demoteOtherAppliedManagedEmailDomains` and `markManagedEmailDomainApplied` execute as two separate raw queries with no enclosing transaction. If two concurrent `applyManagedEmailProvider` calls arrive for the same tenancy (e.g., two browser tabs clicking "Use this domain" simultaneously), the sequence can interleave:

1. Call A demotes Y → VERIFIED
2. Call B demotes X → VERIFIED
3. Call A marks X → APPLIED
4. Call B marks Y → APPLIED

…leaving both X and Y `APPLIED` — exactly the bug this PR was meant to fix. Wrapping both operations in a `$transaction` (or using a single atomic `UPDATE … RETURNING` pattern) would close this window.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Line: 962-967

Comment:
**`activeManagedDomainId` depends on `emailConfig` that isn't refreshed after applying a domain**

`activeManagedDomainId` is derived by matching `emailConfig.managedSubdomain` / `emailConfig.managedSenderLocalPart` against the `domains` list. When the user clicks "Use this domain" in the list, only `refreshDomains()` is called — `emailConfig` (the project config) is not re-fetched. As a result, the "Active" badge and "In use for this project" subtitle won't update until the page is reloaded or the project config is independently invalidated.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Line: 1326-1348

Comment:
**SMTP config fields lost inline validation feedback**

The old render loop included `className={cn(isDirty && isEmpty && "border-destructive")}` and an inline `{field.label} is required` error for each empty SMTP/Resend field. The new code renders the same fields without any per-field error indicator. The save button is still correctly disabled via `canSave`, but users see no visual cue explaining why — especially problematic for the four SMTP fields (host, port, username, password) where any one being blank silently blocks saving.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/backend/src/lib/managed-email-onboarding.tsx
Line: 55-63

Comment:
**Fire-and-forget domain status update has no error handling in mock path**

`runAsynchronously` is used without a `.catch` or `onError` handler. If `updateManagedEmailDomainWebhookStatus` throws (e.g., the row was deleted before the 1-second delay completes), the error is silently swallowed, leaving the mock domain permanently in `pending_verification`. This is dev-only code, but it can cause confusing failures during local testing.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "email settings ui changes" | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx (3)

316-324: toast for "DNS not yet propagated" is borderline — consider an inline note instead.

The user just took an explicit "Check verification" action and the dialog stays open on stage 2. Per the repo guideline ("For blocking alerts and errors, never use toast, as they are easily missed"), this isn't an error, so toast is allowed — but the result of the action is important feedback and is easy to miss, especially if the toaster is out of the user's eye path while they're focused on the modal. Consider rendering it inline (e.g. near the DNS table, similar to how error is displayed) so the feedback stays within the modal's flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
around lines 316 - 324, Replace the transient toast call used for the "DNS not
yet propagated" case with an inline, persistent message rendered inside the
modal (near the DNS records table) so the user sees the result while the dialog
remains open: in the branch that currently calls toast (inside the function
handling the verification response where next.status is neither "verified" nor
"failed"), stop calling toast and instead set a new piece of component state
(e.g., add/use an info state like dnsInfoMessage via setDnsInfoMessage) or reuse
the existing error rendering pattern, then render that message conditionally
next to the DNS table (similar to how setError / error is displayed) so the
feedback remains visible while stage 2 is active. Ensure setStage and setError
logic is unchanged for the other branches.

790-823: Provider cards use transition-all which animates hover-enter too.

Same guideline as above — transition-all on these provider tiles will run on hover-enter and hover-leave. Prefer scoping to the properties you actually animate (border/background/ring) and disabling on hover so only the exit fades.

♻️ Proposed tweak
-                    "relative text-left rounded-xl border p-4 transition-all",
+                    "relative text-left rounded-xl border p-4 transition-[background-color,border-color,box-shadow] hover:transition-none",

As per coding guidelines: "AVOID hover-enter transitions; use only hover-exit transitions. For example, transition-colors hover:transition-none".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
around lines 790 - 823, The provider card button currently uses "transition-all"
which triggers hover-enter animations; update the className on the JSX <button>
(the button rendered for each provider where handleSelectProvider(p.value) is
used and variables like isSaved/isDraft/isSelected are applied) to scope
transitions to only the properties you animate (e.g., border, background, ring,
shadow) and disable hover-enter by adding hover:transition-none; for example
replace "transition-all" with a scoped transition like "transition-colors
transition-shadow" and append "hover:transition-none" so only hover-exit/fade
runs while keeping the rest of the conditional classes
(isSaved/isDraft/isSelected) intact.

219-239: Minor: setTimeout has no unmount cleanup and the hover transition applies on enter.

Two small things in this button:

  • If the dialog/card unmounts within the 1.2 s window after a click, setCopied(false) runs on an unmounted component (React will warn and the state update is wasted). Consider tracking the timeout id in a ref and clearing it in a useEffect cleanup, or gating the setCopied call with an isMounted ref.
  • Per the repo guideline on hover transitions ("AVOID hover-enter transitions; use only hover-exit transitions. For example, transition-colors hover:transition-none"), the bare transition-colors here animates both directions. Add hover:transition-none so only the exit fades.
♻️ Proposed tweak
-      className="shrink-0 p-1 rounded-md hover:bg-foreground/[0.06] text-muted-foreground hover:text-foreground transition-colors"
+      className="shrink-0 p-1 rounded-md hover:bg-foreground/[0.06] text-muted-foreground hover:text-foreground transition-colors hover:transition-none"

As per coding guidelines: "AVOID hover-enter transitions; use only hover-exit transitions. For example, transition-colors hover:transition-none".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
around lines 219 - 239, The CopyButton component sets a timeout without cleanup
and uses a hover-enter transition; fix it by tracking the timeout id in a ref
(e.g., timeoutRef via useRef) and clearing it on unmount in a useEffect cleanup
(or gate setCopied with an isMounted ref) so setCopied(false) won't run after
unmount, and update the button's className to include hover:transition-none
alongside transition-colors (e.g., "transition-colors hover:transition-none") to
avoid hover-enter animations; the key symbols to change are CopyButton,
setCopied/setTimeout, timeoutRef/useEffect (or isMounted ref), and the button
className.
apps/backend/src/lib/managed-email-onboarding.tsx (1)

645-649: Non-atomic demote + mark-applied can transiently leave zero APPLIED domains.

demoteOtherAppliedManagedEmailDomains runs, then markManagedEmailDomainApplied. If the process dies (or the second query errors) between these statements, the tenancy has no row with status = 'APPLIED' even though saveManagedEmailProviderConfig has already rewritten the config to point at the new subdomain. The user-visible display still works (it matches by subdomain), but the DB APPLIED status becomes misleading for any code that queries it directly.

Two safer options:

  1. Swap the order — mark the new domain APPLIED first, then demote siblings. Between the statements you transiently have two APPLIED rows, but never zero, and the invariant "at least one APPLIED whenever the config points at a managed subdomain" holds.
  2. Wrap both raw SQL updates in a prisma.$transaction([...]) so the status transition is atomic.

Option 2 is strictly safer; option 1 is a one-line change. Feel free to defer if this is acceptable given the config is the source of truth for active sender.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 645 - 649,
Wrap the demote+mark operations in a single DB transaction to avoid a transient
state with zero APPLIED rows: instead of sequentially awaiting
demoteOtherAppliedManagedEmailDomains(...) then
markManagedEmailDomainApplied(...), execute their underlying update queries
inside prisma.$transaction([...]) (or refactor those functions to return the
Prisma query promises and call prisma.$transaction([demotePromise,
markPromise])). Ensure you pass tenancyId and domain.id into the transaction
calls so both updates run atomically; if refactoring is needed, make
demoteOtherAppliedManagedEmailDomains and markManagedEmailDomainApplied return
the Prisma query (not perform the await themselves) so they can be combined in
the transaction.
apps/backend/src/lib/managed-email-domains.tsx (1)

184-198: Prefer $executeRaw for non-returning UPDATE.

Since this query returns no rows, $executeRaw (or $executeRawUnsafe) is the more idiomatic Prisma API for a write with no result set — it returns the affected row count and avoids the implicit unknown array allocation from $queryRaw. Functionally equivalent here, so feel free to defer.

♻️ Proposed change
-  await globalPrismaClient.$queryRaw(Prisma.sql`
+  await globalPrismaClient.$executeRaw`
     UPDATE "ManagedEmailDomain"
     SET
       "status" = 'VERIFIED'::"ManagedEmailDomainStatus",
       "appliedAt" = NULL,
       "updatedAt" = CURRENT_TIMESTAMP
     WHERE "tenancyId" = ${options.tenancyId}
       AND "id" <> ${options.keepId}
       AND "status" = 'APPLIED'::"ManagedEmailDomainStatus"
-  `);
+  `;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/managed-email-domains.tsx` around lines 184 - 198,
Replace the use of globalPrismaClient.$queryRaw in
demoteOtherAppliedManagedEmailDomains with the non-returning variant $executeRaw
(or $executeRawUnsafe) since the UPDATE does not return rows; update the call to
globalPrismaClient.$executeRaw(Prisma.sql`...`) while keeping the same SQL and
interpolated options.tenancyId and options.keepId so the query executes and
returns the affected row count instead of an unused result array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx:
- Around line 883-921: The "Use this domain" button is a no-op for rows with
status "applied" that aren't active because isReadyButUnused includes "applied"
but applyManagedEmailProvider short-circuits on applied; restrict the button to
only show for d.status === "verified" by changing the isReadyButUnused
definition (and the JSX condition that renders the DesignButton) to exclude
"applied", and instead render a non-action indicator (e.g., a "Stale/Applied"
label or a "Reapply" link that performs a full reapply flow) for rows where
d.status === "applied" but d.domainId !== activeManagedDomainId so users don’t
get a misleading success toast; update any toast text or handler wrapped by
runAsynchronouslyWithAlert accordingly.

---

Nitpick comments:
In `@apps/backend/src/lib/managed-email-domains.tsx`:
- Around line 184-198: Replace the use of globalPrismaClient.$queryRaw in
demoteOtherAppliedManagedEmailDomains with the non-returning variant $executeRaw
(or $executeRawUnsafe) since the UPDATE does not return rows; update the call to
globalPrismaClient.$executeRaw(Prisma.sql`...`) while keeping the same SQL and
interpolated options.tenancyId and options.keepId so the query executes and
returns the affected row count instead of an unused result array.

In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 645-649: Wrap the demote+mark operations in a single DB
transaction to avoid a transient state with zero APPLIED rows: instead of
sequentially awaiting demoteOtherAppliedManagedEmailDomains(...) then
markManagedEmailDomainApplied(...), execute their underlying update queries
inside prisma.$transaction([...]) (or refactor those functions to return the
Prisma query promises and call prisma.$transaction([demotePromise,
markPromise])). Ensure you pass tenancyId and domain.id into the transaction
calls so both updates run atomically; if refactoring is needed, make
demoteOtherAppliedManagedEmailDomains and markManagedEmailDomainApplied return
the Prisma query (not perform the await themselves) so they can be combined in
the transaction.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx:
- Around line 316-324: Replace the transient toast call used for the "DNS not
yet propagated" case with an inline, persistent message rendered inside the
modal (near the DNS records table) so the user sees the result while the dialog
remains open: in the branch that currently calls toast (inside the function
handling the verification response where next.status is neither "verified" nor
"failed"), stop calling toast and instead set a new piece of component state
(e.g., add/use an info state like dnsInfoMessage via setDnsInfoMessage) or reuse
the existing error rendering pattern, then render that message conditionally
next to the DNS table (similar to how setError / error is displayed) so the
feedback remains visible while stage 2 is active. Ensure setStage and setError
logic is unchanged for the other branches.
- Around line 790-823: The provider card button currently uses "transition-all"
which triggers hover-enter animations; update the className on the JSX <button>
(the button rendered for each provider where handleSelectProvider(p.value) is
used and variables like isSaved/isDraft/isSelected are applied) to scope
transitions to only the properties you animate (e.g., border, background, ring,
shadow) and disable hover-enter by adding hover:transition-none; for example
replace "transition-all" with a scoped transition like "transition-colors
transition-shadow" and append "hover:transition-none" so only hover-exit/fade
runs while keeping the rest of the conditional classes
(isSaved/isDraft/isSelected) intact.
- Around line 219-239: The CopyButton component sets a timeout without cleanup
and uses a hover-enter transition; fix it by tracking the timeout id in a ref
(e.g., timeoutRef via useRef) and clearing it on unmount in a useEffect cleanup
(or gate setCopied with an isMounted ref) so setCopied(false) won't run after
unmount, and update the button's className to include hover:transition-none
alongside transition-colors (e.g., "transition-colors hover:transition-none") to
avoid hover-enter animations; the key symbols to change are CopyButton,
setCopied/setTimeout, timeoutRef/useEffect (or isMounted ref), and the button
className.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80182ca7-2fbd-4f75-9bda-8f35211db4af

📥 Commits

Reviewing files that changed from the base of the PR and between 0006346 and 459589c.

⛔ Files ignored due to path filters (2)
  • apps/dashboard/public/assets/resend-icon-black.svg is excluded by !**/*.svg
  • apps/dashboard/public/assets/resend-icon-white.svg is excluded by !**/*.svg
📒 Files selected for processing (3)
  • apps/backend/src/lib/managed-email-domains.tsx
  • apps/backend/src/lib/managed-email-onboarding.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx

The mock onboarding now inserts domains as pending_verification and flips
them to verified ~1s later (mirroring the real Resend webhook flow), so
the test now asserts the initial pending_verification state and waits
before expecting verified.
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved!

@BilalG1 BilalG1 merged commit 2f71990 into dev Apr 24, 2026
39 checks passed
@BilalG1 BilalG1 deleted the email-server-ui-variations branch April 24, 2026 20:35
@promptless
Copy link
Copy Markdown
Contributor

promptless Bot commented Apr 24, 2026

Promptless prepared a documentation update related to this change.

Triggered by PR #1373

Updated the email documentation to reflect the redesigned Email Server settings UI, including the new visual provider picker with four card options, managed domain stepper flow, DNS records table format, domain status states, and Resend provider option.

Review: Document managed email provider

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants